Summary mode: fix content padding, add expressive delivery, fix map-reduce threshold gap#21
Merged
Merged
Conversation
The configured summary LLM (Qwen3.5-9B) has a 262K-token context, so the old 16K-char single-pass threshold forced most long-form articles through 3-4 sequential LLM calls when one pass would do. Verified live: a 33K-char article's Summary-mode conversion time dropped from 102.2s to 72.0s (~30% faster); summarize alone dropped 51.3s to 13.4s. Map-reduce still triggers correctly for genuinely huge inputs (confirmed with a 226K-char synthetic doc). Full pytest suite passes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Reviewing a real library read surfaced the bug: a 189-word source produced a 243-word "summary" — longer than the article — padded with generic wrap-up sentences not grounded in the source (e.g. "a significant shift toward industry-wide safety standards", "malicious applications" — neither in the original text). The tone prompts said "don't pad" but gave the model no sense of what "short" meant for a given source, just an upper bound it drifted toward regardless of input length. _summarize_once now spells out the source's word count as a concrete anchor, and both tone prompts target roughly half that count, framing 250 words as a ceiling for genuinely long sources rather than a default. Verified: the same article now summarizes to 188 words with zero invented claims, while a 5,240-word Wikipedia article still produces a 280-word summary near the ceiling (long-form coverage unaffected). Full pytest suite passes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
CSM has no direct emotion/prosody control API — sampling temperature is the only delivery lever it exposes, and it was previously fixed once per read from the reading tone. Added _expressive_temperature (speak.py), which nudges the tone's base temperature per chunk from its punctuation (! -> livelier, ? -> questioning, comma-dense with neither -> more measured), so delivery shifts with content instead of staying flat for the whole read. Also nudged both tone prompts (tones.py) to write with natural spoken rhythm instead of a flat, even-register list of facts. Verified with a real Synthesizer (no stub): a mixed-punctuation paragraph produced genuinely different temperatures across its two chunks (0.88 vs 0.77 from a 0.8 base). Granularity is per ~400-char chunk, not per sentence — a documented tradeoff against the existing chunk-size speed/quality knob, not a bug. Full pytest suite passes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
…nks) User asked to pick the best version for voice quality and summary accuracy, explicitly accepting a slighter delay. Switches to the preset already documented in config.yaml's speed/quality guide: precision fp32 (max fidelity) and _MAX_CHARS 200 (best intonation), previously left off in favor of the Fast preset (bf16 / 400). Smaller chunks also sharpen the per-chunk expressive-temperature feature: a dramatic test paragraph went from 2 chunks (a calm sentence dragged into the same chunk/temperature as the exclamation after it) to 5 chunks, each getting its own content-appropriate temperature. Verified live with afplay A/B and a real Synthesizer. Full pytest suite passes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
A uniform _MAX_CHARS cap makes every chunk close to the same length, which reads with a mechanical, same-every-time breath cadence. chunk_text now draws each chunk's cap fresh from [_MIN_CHUNK_CHARS(120), _MAX_CHARS(200)] via _next_chunk_cap(), redrawn per chunk, so consecutive chunks vary in length. The over-long-sentence comma-split safety net still checks against the fixed _MAX_CHARS, not the random cap. This also compounds with the per-chunk expressive-temperature feature: a shorter random cap is more likely to isolate a single sentence (and its own temperature) instead of merging it with a differently-toned neighbor. Verified: same input text produces genuinely different chunk counts/lengths across repeated calls. Existing chunking tests only assert bounds and paragraph-splitting, not exact boundaries, so they hold regardless of which cap is drawn; ran them 20x to rule out flakiness. Full pytest suite passes. Co-Authored-By: Claude Sonnet 5 <[email protected]>
User asked to stick with bf16 after trying the fp32 Max-quality preset — bf16 has no audible quality loss at normal listening per the engine's own docs, so there's no real quality cost to reverting it, just the ~6% speed saving back. The 200-char / randomized chunk-boundary changes (the actual driver of the requested voice-quality/expression improvements) are unaffected. Co-Authored-By: Claude Sonnet 5 <[email protected]>
…-padding-short-articles # Conflicts: # docs/PLAN.md
Confirmed the merge actually fixes the reported 140.4s conversion time: same article, full pipeline, before vs after the merge — 140.4s -> 97.0s (31% faster), and the map-reduce path's 250-word-ceiling overshoot (403 words) improves to 313 (better, not yet fully fixed). Co-Authored-By: Claude Sonnet 5 <[email protected]>
…iling Tracked for next session: the word-count-anchor fix helps (403 -> 313 words on the reported slow article) but doesn't fully hold the 250-word hard limit on longer single-pass summaries. Also folds the padding/expression/chunking work from this branch into the Recently shipped list. Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ast chunk band, fragment drop
- chunk band [120,200] -> [280,400]: the low band measured 2.5-3x the chunks
(= CSM prefills = time) of fixed 400; randomization itself is free, so the
cadence variation is kept at the fast end
- enforce the 250-word summary ceiling in code (_trim_to_word_ceiling,
sentence-boundary clip at SUMMARY_WORD_CEILING) instead of prompt-only
- thread the original source word count through map-reduce so the reduce
step's length anchor stops measuring the compressed digests
- never drop a sub-_MIN_CHARS fragment mid-paragraph ("Wow!" was lost ~21%
of runs under low random caps); space-split comma-free runs > _MAX_CHARS
- dedupe the tone prompts' length policy into _LENGTH_RULES (one source of truth)
- tests: 38 -> 44 (fragment retention, comma-free hard split, 4x ceiling trim)
Co-Authored-By: Claude Fable 5 <[email protected]>
Reviewing a live read showed the summary faithfully reproducing the blog's closing acknowledgements and 'share your feedback' invite — grounded in the source, but zero-information listening. _LENGTH_RULES now orders the model to omit acknowledgements, community thanks, calls to action, and contact invites entirely and end on a content point. A softer 'skip them' phrasing was not enough (the tail still appeared, paraphrased); the imperative form verified clean on two live regenerations of the same article. Co-Authored-By: Claude Fable 5 <[email protected]>
… in the player The reading pace felt slow; measurement showed the speech itself is 170-187 wpm (normal) so pace is listener taste, and CSM has no speed control — the right altitude is playback. afplay -r RATE -q 1 gives pitch-preserving rate scaling: +/- in the player steps 0.1x (0.5-2x) live, restarting at the current position via the existing seek-slice mechanism; /speed <x> sets it from the input screen; rate shows beside the progress bar when not 1x and persists to ~/.readback/cli.json. elapsed now advances at rate x wall time (audio position) so seek and the synced transcript stay aligned at any rate. Verified via tmux-driven CLI: set/step/persist, pause/resume/seek/transcript at non-1x rates, no orphaned afplay on quit. Co-Authored-By: Claude Fable 5 <[email protected]>
Summary quality/delivery fixes (padding, ceiling enforcement, map-reduce anchor, expressive chunking) plus the CLI playback speed controller land in this release. Bumped all four anchors (pyproject.toml, src/readback/__init__.py, src/cli/package.json, src/dashboard/package.json) and refreshed the CLAUDE.md Version section + ARCHITECTURE.md header + JOURNEY.md stack snapshot. CLI binary rebuilt to pick up the new banner. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
!/?/dense commas) instead of staying flat for the whole read.summary_max_chars16K→60K (this branch had diverged frommainbefore that fix landed) — closes a real gap where medium-long articles were needlessly map-reducing._MAX_CHARS: 200for better intonation; keepprecision: bf16(no audible quality loss vs fp32, so no reason to pay the ~6% fp32 cost).Verified
docs/ROADMAP.md).Synthesizer(no stub) produces genuinely different temperatures across chunks; played back withafplayfor a live A/B.pytestsuite: 38/38 pass throughout.Test plan
pytest🤖 Generated with Claude Code